package wonder.tools;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

/**
 * <p>
 * Config class.
 * </p>
 *
 * @author u211354
 * @version $Id: $
 */
public class Config {
	private final HashMap<Object, ConfigEntry> config;

	/**
	 * <p>
	 * Constructor for Config.
	 * </p>
	 */
	public Config() {
		config = new HashMap<Object, ConfigEntry>();
	}

	/**
	 * Loads the properties from the specified {@code File}.
	 *
	 * @param file
	 *            a {@link java.io.File} object.
	 * @throws java.io.IOException
	 *             if any.
	 */
	public void load(File file) throws IOException {
		if (file != null) {
			load(file.toURI().toURL());
		}
	}

	/**
	 * Loads the properties from the specified {@code URL}.
	 *
	 * @param url
	 *            a {@link java.net.URL} object.
	 * @throws java.io.IOException
	 *             if any.
	 */
	public void load(URL url) throws IOException {
		if (url != null) {
			InputStream in = null;
			try {
				in = url.openStream();
				load(in);
			} finally {
				if (in != null) {
					try {
						in.close();
					} catch (Exception e) {
					}
				}
			}
		}
	}

	/**
	 * Loads the properties from the specified {@code InputStream}.
	 *
	 * @param in
	 *            a {@link java.io.InputStream} object.
	 * @throws java.io.IOException
	 *             if any.
	 */
	public void load(InputStream in) throws IOException {
		Properties props = new Properties();
		props.load(in);
		load(props);
	}

	/**
	 * Loads the properties from the specified {@code Map}.
	 *
	 * @param props
	 *            a {@link java.util.Map} object.
	 */
	public void load(Map<Object, Object> props) {
		Set<Entry<Object, Object>> set = props.entrySet();
		for (Entry<Object, Object> pentry : set) {
			setEntry(pentry.getKey(), pentry.getValue());
		}
	}

	/**
	 * Returns the object to which the specified key is mapped or {@code null}
	 * if this config contains no mapping for the key or if the key is
	 * {@code null}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Object} object.
	 */
	public Object getEntry(Object key) {
		if (key != null) {
			ConfigEntry centry = config.get(key);
			if (centry != null)
				return centry.getValue();
		}
		return null;
	}

	/**
	 * Associates the specified value with the specified key in this config. If
	 * config map previously contained a mapping for the key, the old value is
	 * replaced.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @param val
	 *            a {@link java.lang.Object} object.
	 */
	public void setEntry(Object key, Object val) {
		if (key != null) {
			if (val == null) {
				deleteEntry(key);
			}
			ConfigEntry centry = config.get(key);
			if (centry == null) {
				centry = new ConfigEntry(key, val);
				config.put(key, centry);
			}
			centry.setValue(val);
		}
	}

	/**
	 * Removes the mapping for the specified key from this config if present.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 */
	public void deleteEntry(Object key) {
		if (key != null) {
			ConfigEntry centry = config.get(key);
			if (centry != null) {
				centry.setValue(null);
			}
			if (centry.isEmpty()) {
				config.remove(key);
			}
		}
	}

	/**
	 * <p>dispose</p>
	 */
	public void dispose() {
		config.clear();
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code String}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.String} object.
	 */
	public String getString(Object key) {
		Object val = getEntry(key);
		if (val != null)
			return val.toString();
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@link Boolean}. Returns {@code null} if the value is not a
	 * {@code Boolean} or a {@code String} compatible with
	 * {@link Boolean#valueOf(String)}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Boolean} object.
	 */
	public Boolean getBoolean(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Boolean)
				return (Boolean) val;
			if (val instanceof String)
				return Boolean.valueOf((String) val);
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@link Integer}. Returns {@code null} if the value is not a
	 * {@code Integer} or a {@code String} compatible with
	 * {@link Integer#valueOf(String)}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Integer} object.
	 */
	public Integer getInteger(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Integer)
				return (Integer) val;
			if (val instanceof String)
				return Integer.valueOf((String) val);
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@link Long}. Returns {@code null} if the value is not a {@code Long} or
	 * a {@code String} compatible with {@link Long#valueOf(String)}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Long} object.
	 */
	public Long getLong(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Long)
				return (Long) val;
			if (val instanceof String)
				return Long.valueOf((String) val);
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@link Float}. Returns {@code null} if the value is not a {@code Float}
	 * or a {@code String} compatible with {@link Float#valueOf(String)}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Float} object.
	 */
	public Float getFloat(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Float)
				return (Float) val;
			if (val instanceof String)
				return Float.valueOf((String) val);
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@link Double}. Returns {@code null} if the value is not a {@code Double}
	 * or a {@code String} compatible with {@link Double#valueOf(String)}.
	 *
	 * @param key
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Double} object.
	 */
	public Double getDouble(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Double)
				return (Double) val;
			if (val instanceof String)
				return Double.valueOf((String) val);
		}
		return null;
	}

	/**
	 * Returns a unmodifiable {@code Set} of the keys contained in this config.
	 *
	 * @return a {@link java.util.Set} object.
	 */
	public Set<Object> getAllKeys() {
		return Collections.unmodifiableSet(config.keySet());
	}

	/**
	 * <p>addConfigListener</p>
	 *
	 * @param key a {@link java.lang.Object} object.
	 * @param listener a {@link wonder.tools.ConfigListener} object.
	 */
	public void addConfigListener(Object key, ConfigListener listener) {
		ConfigEntry centry = config.get(key);
		if (centry == null) {
			centry = new ConfigEntry(key);
		}
		centry.addConfigListener(listener);
	}

	/**
	 * <p>removeConfigListener</p>
	 *
	 * @param key a {@link java.lang.Object} object.
	 * @param listener a {@link wonder.tools.ConfigListener} object.
	 */
	public void removeConfigListener(Object key, ConfigListener listener) {
		ConfigEntry centry = config.get(key);
		if (centry != null) {
			centry.removeConfigListener(listener);
			if (centry.isEmpty()) {
				config.remove(key);
			}
		}
	}

	protected class ConfigEntry {
		Object value;
		ConfigListener[] listeners;

		/**
		 * Constructs an entry with its value set to {@code null}.
		 */
		ConfigEntry(Object key) {
			this(key, null);
		}

		ConfigEntry(Object key, Object value) {
			this.value = value;
			listeners = new ConfigListener[0];
		}

		Object getValue() {
			return value;
		}

		Object setValue(Object newValue) {
			Object oldValue = value;
			value = newValue;

			ConfigEvent event = new ConfigEvent(Config.this, oldValue, newValue);
			for (ConfigListener listener : listeners) {
				if (value != null) {
					listener.valueChanged(event);
				} else {
					listener.valueDeleted(event);
				}
			}
			return oldValue;
		}

		/**
		 * Adds a {@code ConfigListener} to this entry.
		 */
		void addConfigListener(ConfigListener listener) {
			ConfigListener[] newListeners = new ConfigListener[listeners.length + 1];
			System.arraycopy(listeners, 0, newListeners, 0, listeners.length);
			newListeners[listeners.length] = listener;
			listeners = newListeners;
		}

		/**
		 * Removes a {@code ConfigListener} from this entry.
		 */
		void removeConfigListener(ConfigListener listener) {
			for (int i = 0; i < listeners.length; ++i) {
				if (!listeners[i].equals(listener)) {
					continue;
				}

				ConfigListener[] newListeners = new ConfigListener[listeners.length + 1];
				System.arraycopy(listeners, 0, newListeners, 0, i);
				System.arraycopy(listeners, i + 1, newListeners, i,
						newListeners.length - i);
				listeners = newListeners;

				break;
			}
		}

		/**
		 * Returns {@code true} if this entry contains no listeners and if the
		 * value is {@code null}.
		 */
		boolean isEmpty() {
			return value == null && listeners.length == 0;
		}
	}
}
